﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WordpressRestApi
{
    /// <summary>Enumerable for API driven collection.</summary>
    /// <typeparam name="T">Type of the enumeration</typeparam>
    /// <typeparam name="PL">Type of the payload produced by the API.</typeparam>
    internal class ApiAsyncEnumerable<T, PL> : TokenOwnerBase, IAsyncEnumerable<T> where PL : class
    {
        #region Inner types
        private class ApiAsyncEnumerator : ApiClientBase, IAsyncEnumerator<T>
        {
            private readonly string _startUrl;
            private readonly Func<PL, IEnumerable<T>> _getCollection;
            private readonly Func<PL, int, bool> _hasMorePredicate;
            private readonly Func<PL, int, string> _getNextUrl;

            private bool _hasStarted = false;
            private bool _hasFinished = false;
            private int _page = 0;
            private IEnumerable<T> _currentBatch;
            private PL _lastPayload;

            public ApiAsyncEnumerator(
                TokenInfo token,
                string startUrl,
                Func<PL, IEnumerable<T>> getCollection,
                Func<PL, int, bool> hasMorePredicate,
                Func<PL, int, string> getNextUrl)
            : base(token)
            {
                if (string.IsNullOrWhiteSpace(startUrl))
                {
                    throw new ArgumentNullException(nameof(startUrl));
                }
                if (getCollection == null)
                {
                    throw new ArgumentNullException(nameof(getCollection));
                }
                if (hasMorePredicate == null)
                {
                    throw new ArgumentNullException(nameof(hasMorePredicate));
                }
                if (getNextUrl == null)
                {
                    throw new ArgumentNullException(nameof(getNextUrl));
                }

                _startUrl = startUrl;
                _getCollection = getCollection;
                _hasMorePredicate = hasMorePredicate;
                _getNextUrl = getNextUrl;
            }

            IEnumerable<T> IAsyncEnumerator<T>.CurrentBatch
            {
                get
                {
                    if (!_hasStarted)
                    {
                        throw new InvalidOperationException("Enumeration hasn't started yet");
                    }
                    else if (_hasFinished)
                    {
                        throw new InvalidOperationException("Enumeration is finished");
                    }
                    else
                    {
                        return _currentBatch;
                    }
                }
            }

            async Task<bool> IAsyncEnumerator<T>.MoveNextAsync()
            {
                if (_hasFinished)
                {
                    throw new InvalidOperationException("Enumeration is finished");
                }
                else if (!_hasStarted)
                {
                    _lastPayload = await GetRequestAsync<PL>(_startUrl);
                    _currentBatch = _getCollection(_lastPayload);
                    _hasStarted = true;

                    return true;
                }
                else if (_hasMorePredicate(_lastPayload, _page))
                {
                    ++_page;

                    var nextUrl = _getNextUrl(_lastPayload, _page);

                    _lastPayload = await GetRequestAsync<PL>(nextUrl);
                    _currentBatch = _getCollection(_lastPayload);

                    return true;
                }
                else
                {
                    _hasFinished = true;

                    return false;
                }
            }
        }
        #endregion

        private readonly string _startUrl;
        private readonly Func<PL, IEnumerable<T>> _getCollection;
        private readonly Func<PL, int, bool> _hasMorePredicate;
        private readonly Func<PL, int, string> _getNextUrl;

        public ApiAsyncEnumerable(
            TokenInfo token,
            string startUrl,
            Func<PL, IEnumerable<T>> getCollection,
            Func<PL, int, bool> hasMorePredicate,
            Func<PL, int, string> getNextUrl)
            : base(token)
        {
            if (string.IsNullOrWhiteSpace(startUrl))
            {
                throw new ArgumentNullException(nameof(startUrl));
            }
            if (getCollection == null)
            {
                throw new ArgumentNullException(nameof(getCollection));
            }
            if (hasMorePredicate == null)
            {
                throw new ArgumentNullException(nameof(hasMorePredicate));
            }
            if (getNextUrl == null)
            {
                throw new ArgumentNullException(nameof(getNextUrl));
            }

            _startUrl = startUrl;
            _getCollection = getCollection;
            _hasMorePredicate = hasMorePredicate;
            _getNextUrl = getNextUrl;
        }

        IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator()
        {
            return new ApiAsyncEnumerator(Token, _startUrl, _getCollection, _hasMorePredicate, _getNextUrl);
        }
    }
}